package edu.up.firesawakening;

import java.util.Collections;
import java.util.Vector;

import edu.up.game.*;

/**
 * Represents the current state of the game
 * 
 * @author Alex Hanemann
 * @author Matt Delaney
 * @author Viet Phan
 * @version 19 November 2012
 */
public class TacticsGame extends LocalGame {

	/**
	 * Serial ID
	 */
	private static final long serialVersionUID = -5722774387553498806L;

	public static int NO_WINNER = -1;

	/**
	 * Contains the 'map' on which the game is played. Units are also contained
	 * on the tiles on this array.
	 */
	protected Tile[][] m_map;

	/**
	 * Types of move we can have
	 */
	public enum MoveType {
		MOVE, ATTACK, EMPTY_ATTACK, PASS, NONE, SELF
	};

	/**
	 * Constructor. Requires a map (including units!)
	 * 
	 * @param initConfig
	 *            the configuration for this game
	 * @param initTurn
	 *            whose turn to start the game with
	 * @param mapIn
	 *            the map to play on
	 */
	public TacticsGame(GameConfig initConfig, int initTurn, Tile[][] mapIn) {
		super(initConfig, initTurn);
		whoseTurn = initTurn;
		m_map = mapIn;
	}

	/**
	 * 
	 * @return a copy of the game's map
	 */
	public Tile[][] getMap() {
		return m_map;
	}

	/**
	 * Checks to see if a unit can move to a particular space
	 * 
	 * @param src
	 *            the source tile
	 * @param tile
	 *            the target tile
	 * 
	 * @return the type of move the unit can make to the space
	 */
	public MoveType checkTarget(Tile src, Tile dest) {

		if (src == null || dest == null || !src.isOccupied())
			return MoveType.NONE;

		if (src == dest)
			return MoveType.SELF;

		Unit unit = src.getUnit();

		// If there are no moves, try updating
		if (unit.getActions().isEmpty())
			availableActions(src);

		// Check again, if there still aren't any we're done here
		if (unit.getActions().isEmpty())
			return MoveType.NONE;

		for (TacticsAction a : unit.getActions()) {
			if (a.getStart() == src && a.getTarget() == dest) {
				if (a instanceof TacticsMoveAction) {
					return MoveType.MOVE;
				} else if (a instanceof TacticsAttackAction) {
					return MoveType.ATTACK;
				}
			}
		}

		return MoveType.NONE;
	}

	/**
	 * Determines all possible moves a given unit can take and gives them to the
	 * unit.
	 * 
	 * @param tile
	 *            the tile to move from
	 * @return a board array indicating which positions should show move/attack
	 *         indicators. Used by player for display
	 */
	public Move[][] availableActions(Tile tile) {

		Move[][] returnArray = new Move[m_map.length][m_map[0].length];

		for (int i = 0; i < returnArray.length; i++)
			for (int j = 0; j < returnArray[0].length; j++)
				returnArray[i][j] = new Move(MoveType.NONE, tile, m_map[i][j],
						0);

		// Should be a unit here
		if (!tile.isOccupied())
			return returnArray;

		Unit unit = tile.getUnit();

		// Should be owned by the current player
		if (unit.getOwner() != whoseTurn)
			return returnArray;

		// tiles we haven't visited
		Vector<Tile> openList = new Vector<Tile>();

		// tiles we've visited
		Vector<Tile> closedList = new Vector<Tile>();

		// tiles with an attack
		Vector<Tile> attackList = new Vector<Tile>();

		Vector<Tile> closedAttackList = new Vector<Tile>();

		tile.currCost = 0;

		openList.add(tile);

		int currX, currY, currCost;
		int movesLeft = unit.getMovesLeft();

		Tile currentTile;

		// Find all possible moves
		while (!openList.isEmpty()) {

			Collections.sort(openList);

			// Get current tile values
			currentTile = openList.get(0);
			currX = currentTile.getX();
			currY = currentTile.getY();
			currCost = currentTile.currCost;

			// Check all directions for possible moves
			checkTile(tile, currentTile, currX, currY - 1, currCost, movesLeft,
					openList, closedList, attackList);
			checkTile(tile, currentTile, currX, currY + 1, currCost, movesLeft,
					openList, closedList, attackList);

			checkTile(tile, currentTile, currX - 1, currY, currCost, movesLeft,
					openList, closedList, attackList);
			checkTile(tile, currentTile, currX + 1, currY, currCost, movesLeft,
					openList, closedList, attackList);

			openList.remove(currentTile);
			closedList.add(currentTile);
		}

		for (Tile t : closedList) {

			int cost = t.currCost;
			if (t.isOccupied() && t.getUnit().getOwner() == unit.getOwner())
				returnArray[t.getX()][t.getY()] = new Move(MoveType.PASS, tile,
						t, cost);
			else
				returnArray[t.getX()][t.getY()] = new Move(MoveType.MOVE, tile,
						t, cost);
		}

		Vector<Tile> openAttackList = new Vector<Tile>();

		for (int i = 1; i <= unit.getAttackRange(); i++) {
			for (Tile t : attackList) {

				currX = t.getX();
				currY = t.getY();

				if (currX >= 0 && currY + 1 >= 0 && currX < m_map.length
						&& currY + 1 < m_map[0].length) {
					openAttackList.add(m_map[currX][currY + 1]);
				}

				if (currX >= 0 && currY - 1 >= 0 && currX < m_map.length
						&& currY - 1 < m_map[0].length) {
					openAttackList.add(m_map[currX][currY - 1]);
				}

				if (currX + 1 >= 0 && currY >= 0 && currX + 1 < m_map.length
						&& currY < m_map[0].length) {
					openAttackList.add(m_map[currX + 1][currY]);
				}

				if (currX - 1 >= 0 && currY >= 0 && currX - 1 < m_map.length
						&& currY < m_map[0].length) {
					openAttackList.add(m_map[currX - 1][currY]);
				}

				closedAttackList.add(t);
			}

			attackList.clear();
			attackList.addAll(openAttackList);
		}

		for (Tile t : closedAttackList) {
			if (closedList.contains(t))
				continue;

			int cost = t.currCost;

			if (t.isOccupied()) {
				if (t.getUnit().getOwner() == unit.getOwner()) {
					returnArray[t.getX()][t.getY()] = new Move(MoveType.PASS,
							tile, t, cost);
				} else if (unit.hasAttackAvailable()) {
					if (distanceBetween(tile, t) <= unit.getAttackRange()) {
						returnArray[t.getX()][t.getY()] = new Move(
								MoveType.ATTACK, tile, t, cost);
					} else {
						returnArray[t.getX()][t.getY()] = new Move(
								MoveType.EMPTY_ATTACK, tile, t, cost);
					}
				}
			} else {
				if (unit.hasAttackAvailable())
					returnArray[t.getX()][t.getY()] = new Move(
							MoveType.EMPTY_ATTACK, tile, t, cost);
				else
					returnArray[t.getX()][t.getY()] = new Move(MoveType.NONE,
							tile, t, cost);
			}

		}

		returnArray[tile.getX()][tile.getY()] = new Move(MoveType.SELF, tile,
				tile, 0);

		return returnArray;
	}

	public Move[][] availableActions(int x, int y) {
		return availableActions(m_map[x][y]);
	}

	/**
	 * Helper method for availableMoves
	 * 
	 * @param x
	 * @param y
	 * @param cost
	 * @param movesLeft
	 * @param openList
	 * @param closedList
	 * 
	 * @return whether or not this tile is in the fringe
	 */
	protected void checkTile(Tile source, Tile previous, int x, int y,
			int cost, int movesLeft, Vector<Tile> openList,
			Vector<Tile> closedList, Vector<Tile> attackList) {
		int newCost;
		boolean friendly = false;

		if (previous.isOccupied() && previous != source)
			friendly = previous.getUnit().getOwner() == whoseTurn;

		// Make sure this tile is on the map
		if (y < 0 || x < 0 || x >= m_map.length || y >= m_map[0].length) {
			return;
		}

		Tile tile = m_map[x][y];

		if (closedList.contains(tile) || attackList.contains(tile))
			return;

		// If we're not next to an enemy, find the cost of the move
		// Ignoring friendly units for now, they'll be removed later
		if (!tile.isOccupied() || tile.getUnit().getOwner() == whoseTurn) {
			newCost = tile.getMoveCost() + cost;

			// If we can move there, add the tile to open
			if (newCost <= movesLeft) {
				tile.currCost = newCost;
				openList.add(tile);
				return;
			}
		}

		if (!friendly) {
			attackList.add(tile);
		} else {
			attackList.add(previous);
		}
	}

	/**
	 * Gets a game state for a particular player to modify at will.
	 * 
	 * @param playerIndex
	 *            the player whose perspective to get a state for
	 * @return a copy of the game state as seen by the given player
	 */
	@Override
	public LocalGame getPlayerState(int playerIndex) {
		Tile[][] newMap = new Tile[m_map.length][m_map[0].length];

		for (int x = 0; x < newMap.length; x++)
			for (int y = 0; y < newMap[0].length; y++)
				newMap[x][y] = m_map[x][y].clone();

		TacticsGame playerCopy = new TacticsGame(getConfig(), whoseTurn(),
				newMap);
		return playerCopy;
	}

	/**
	 * Commits a particular action to the game state.
	 * 
	 * @param action
	 *            the action to apply
	 */
	@Override
	public void applyAction(GameAction action) {
		TacticsAction ta = null;

		if (action instanceof TacticsAction) {
			ta = (TacticsAction) action;

			if (ta instanceof TacticsMoveAction) {
				Tile target = m_map[ta.getTargetX()][ta.getTargetY()];
				Tile source = m_map[ta.getStartX()][ta.getStartY()];

				// This shouldn't happen
				if (!source.isOccupied())
					return;

				target.removeUnit();
				target.setUnit(source.getUnit());

				target.getUnit().setMovesLeft(
						target.getUnit().getMovesLeft() - ta.getMoveCost());
				source.removeUnit();

			} else if (ta instanceof TacticsAttackAction) {
				Unit target = m_map[ta.getTargetX()][ta.getTargetY()].getUnit();
				Unit source = m_map[ta.getStartX()][ta.getStartY()].getUnit();

				source.attack(target);
				source.setAttackAvailable(false);
				source.setMovesLeft(0);

				if (!m_map[ta.getTargetX()][ta.getTargetY()].getUnit()
						.isAlive())
					m_map[ta.getTargetX()][ta.getTargetY()].removeUnit();
			}
		} else {
			// Advance turn
			whoseTurn = whoseTurn == 1 ? 0 : 1;

			for (int i = 0; i < m_map.length; i++) {
				for (int j = 0; j < m_map[0].length; j++) {
					if (m_map[i][j].isOccupied()) {

						m_map[i][j].getUnit().setAttackAvailable(true);
						m_map[i][j].getUnit().setMovesLeft(
								m_map[i][j].getUnit().getMoveRange());
					}
				}
			}
		}

	}

	/**
	 * @return whether or not the game is over
	 */
	@Override
	public boolean isGameOver() {
		return getWinnerId() != -1;
	}

	/**
	 * @return the id of the player who has won the game. If the game is not
	 *         over yet, the output of this method is undefined.
	 */
	@Override
	public int getWinnerId() {

		boolean player0HasCastle = false;
		boolean player1HasCastle = false;

		// count number of units left alive
		int player1Units = 0;
		int player0Units = 0;

		// look for units and castles
		for (int x = 0; x < m_map.length; x++) {
			for (int y = 0; y < m_map[x].length; y++) {
				if (m_map[x][y].isOccupied()) {
					Unit u = m_map[x][y].getUnit();

					if (u.getOwner() == 1) {
						if (u instanceof Castle) {
							player1HasCastle = true;
						} else {
							player1Units++;
						}
					} else {
						if (u instanceof Castle) {
							player0HasCastle = true;
						} else {
							player0Units++;
						}
					}
				}
			}
		}

		// Check if a player is out of units or has no castle
		if (player1Units <= 0 || !player1HasCastle) {
			return 0;
		}

		if (player0Units <= 0 || !player0HasCastle) {
			return 1;
		}

		// If neither of the previous checks passed, the game is not over
		return NO_WINNER;
	}

	/**
	 * @param one
	 *            first tile
	 * @param two
	 *            second tile
	 * @return the distance between the two tiles
	 */
	public static double distanceBetween(Tile one, Tile two) {
		return Math.abs(two.getX() - one.getX())
				+ Math.abs(two.getY() - one.getY());
	}

	/**
	 * A simple data structure representing a move
	 * 
	 * @author Alex Hanemann
	 */
	public class Move {
		public MoveType move;
		public Integer cost;
		public Tile start, target;

		public Move(MoveType m, Tile s, Tile d, Integer c) {
			start = s;
			target = d;
			move = m;
			cost = c;
		}
	}
}
